#!/usr/bin/env python3
# Copyright (c) 2025 pony working team
# SPDX-License-Identifier: MulanPSL-2.0

import os
import re
import time
import pickle
import shutil
import tempfile
import configparser

from kconfiglib import Kconfig

root_dir = os.path.abspath(os.path.dirname(__file__) + "/../..")

project = None
kconfig = None

# build flag ini file
bdf_ini = None
bdfname = "flags.ini"

prjname = os.path.basename(os.getcwd()) + ".prj"

prj_kconfig = '''\
# created by project maintenance tool, do not edit it!

# add timestamp in the name to avoid potential name conflict
config PROJECT_{0}
    def_bool y
    select BOARD_{1}

source "{2}/Kconfig"

menu "target (soc and board) configuration"

source "{3}/Kconfig"
{4}
endmenu

'''


def _relcurpath(path):
    return os.path.relpath(path, os.path.curdir)


def create_dir(path, force, update):
    if os.path.isfile(path):
        print("cannot create dir " + path + ", it is a file!")
        quit()

    if not os.path.isdir(path):
        os.makedirs(path)

    if not os.path.isdir(path):
        print("failed to create dir " + path)
        quit()

    if len(os.listdir(path)) != 0:
        if force:
            shutil.rmtree(path)
        elif update:
            pass
        else:
            print("dir " + path + " not empty!")
            quit()


def get_bdf_ini(name, prj):
    global bdf_ini

    if not project:
        return None

    if bdf_ini:
        return bdf_ini

    if not os.path.exists(name):
        return None

    # refresh IDE project only if the build flags file changed
    if os.path.exists(bdfname):
        with open(bdfname,  "r") as old:
            with open(name, "r") as new:
                if old.readlines() == new.readlines() and os.path.exists(prj):
                    os.remove(name)
                    quit()

        os.remove(bdfname)

    os.rename(name, bdfname)

    bdf_ini = configparser.ConfigParser()
    bdf_ini.read_file(open(bdfname))

    return bdf_ini


def __get_socs(dir, socs):
    for name in os.listdir(dir):
        path = os.path.join(dir, name)
        if os.path.isdir(path):
            __get_socs(path, socs)
            continue
        match = re.match(r"(.+)\.config", name)
        if not match:
            continue
        soc = match.groups()[0]
        if soc == "soc":
            continue
        if soc in socs:
            print("redefined SOC " + soc + " at:")
            print(os.path.join(dir, name))
            print("and\n" + socs[soc])
            quit()
        # such as: socs["stm32f429igtx"] = xxx/target/soc/st/stm32/stm32f4/stm32f429igtx.config
        socs[soc] = os.path.join(dir, name)


def get_socs(root):
    socs = {}
    __get_socs(os.path.join(root, "target/soc"), socs)
    return socs


def get_soc(board):
    # such as: xxx/target/board/pony/stm32/stm32f429_fire/Kconfig
    kconf = Kconfig(os.path.join(board, "Kconfig"))
    if ("SOC" not in kconf.syms) or (not kconf.syms["SOC"].str_value):
        print("invalid board, SOC is not defined!")
        quit
    return kconf.syms["SOC"].str_value


def __get_boards(dir, boards):
    for name in os.listdir(dir):
        path = os.path.join(dir, name)
        if os.path.isfile(path):
            continue
        if os.path.isfile(os.path.join(path, "Kconfig")):
            # such as: boards["stm32f429_fire"] = xxx/target/board/pony/stm32/stm32f29_fire
            boards[name] = path
        else:
            __get_boards(path, boards)


def get_boards(root=root_dir):
    boards = {}
    __get_boards(os.path.join(root, "target/board"), boards)
    return boards


def get_project():
    global project

    if project:
        return project

    if os.path.exists(prjname):
        with open(prjname, "rb") as f:
            project = pickle.load(f)

    return project


def get_kconfig():
    global kconfig

    if kconfig:
        return kconfig

    project = get_project()
    if not project:
        return None

    os.environ.setdefault("KCONFIG_CONFIG", "config/kconfig.mk")

    kconfig = Kconfig("Kconfig")

    kconfig.disable_redun_warnings()
    kconfig.disable_override_warnings()

    # load project kconfig.mk
    if os.path.isfile("config/kconfig.mk"):
        kconfig.load_config("config/kconfig.mk", False)
    else:
        soc_common = os.path.join(project["soc_dir"], "soc.config")
        if os.path.isfile(soc_common):
            kconfig.load_config(soc_common, False)

        # load soc.config
        soc_config = project["soc_cfg"]
        kconfig.load_config(soc_config, False)

        # load board.config
        bsp_config = os.path.join(project["board_dir"], "board.config")
        if os.path.isfile(bsp_config):
            kconfig.load_config(bsp_config, False)

    return kconfig


def parse_workspace(workspace, force=False, update=False):
    global project
    global prjname

    if workspace:
        proj, board = workspace
        prjname = proj

        boards = get_boards(root_dir)

        if not board in boards:
            print("board <" + board +
                  "> not supported! Supported boards are: " + str(list(boards)))
            quit()

        board_dir = boards[board]

        socs = get_socs(root_dir)
        soc = get_soc(board_dir)  # such as: "stm32f429igtx"

        if not soc in socs:
            print("soc <" + soc +
                  "> not supported! Supported socs are: " + str(list(socs)))
            quit()

        soc_cfg = socs[soc]
        soc_dir = os.path.dirname(soc_cfg)

        create_dir(proj, force, update)
        create_dir(proj + "/config", force, update)

        # now, goto the project dir
        os.chdir(proj)

        project = {
            "root": _relcurpath(root_dir),
            "board": board,
            "board_dir": _relcurpath(board_dir),
            "soc": soc,
            "soc_dir": _relcurpath(soc_dir),
            "soc_cfg": _relcurpath(soc_cfg)
        }

        # create kconfig
        with open("Kconfig", "w", encoding="utf-8") as f:
            kconfig_list = []
            kconfig_dir = soc_dir
            while kconfig_dir != root_dir:
                if os.path.isfile(os.path.join(kconfig_dir, "Kconfig")):
                    kconfig_list.insert(
                        0, os.path.join(_relcurpath(kconfig_dir), "Kconfig"))
                kconfig_dir = os.path.dirname(kconfig_dir)

            f.write(prj_kconfig.format(int(time.time()), board.upper(), _relcurpath(root_dir),
                                       _relcurpath(board_dir),
                                       "".join(["source \"" + x + "\"\n" for x in kconfig_list])).replace('\\', '/'))

        kconf = get_kconfig()

        # create Makefile
        with open("Makefile", "w", encoding="utf-8") as f:
            f.write("# created by project maintenance tool, do not edit it!\n")
            f.write("root_dir := $(abspath $(CURDIR)/%s)\n" %
                    (_relcurpath(root_dir).replace('\\', '/')))
            f.write("include $(root_dir)/scripts/make/entry.mk")
            f.write("\n")
    else:
        project = get_project()
        if not project:
            print("Error: not in a project workspace!")
            quit()

    return project


def set_config(content):
    if not content:
        return

    kconf = get_kconfig()

    tmphandle, tmppath = tempfile.mkstemp(prefix="rtcfg.", text=True)
    tmpfile = os.fdopen(tmphandle, "w")

    for s in content:
        match = re.match("CONFIG_([^=]+)=(.*)", s)
        if not match:
            print(s + " not match the 'CONFIG_XX=x' format, assignment ignored.")
            continue
        name, val = match.groups()
        if name not in kconf.syms:
            print("kconfig symbol CONFIG_" + name +
                  " not exist, assignment ignored.")
        else:
            tmpfile.write(s + "\n")

    tmpfile.close()
    warn_to_stderr = kconf.warn_to_stderr
    # do not put warnings to console directly, we will handle it (strip the temp file name)
    kconf.warn_to_stderr = False
    kconf.load_config(tmppath, False)
    kconf.warn_to_stderr = warn_to_stderr
    os.remove(tmppath)

    # handle warnings and ignore the temp file name in it
    for e in kconf.warnings:
        print(re.match (".*(warning:.*)", e).groups()[0])

    kconf.write_config("config/kconfig.mk")
    kconf.write_autoconf("config/kconfig.h")
